HyML (acronym for Hy Markup Language) is a set of macros to generate XML, XHTML, and HTML code in Hy.
HyML MiNiMaL (ml
) macro is departed from the more extensive document and validation oriented full version of HyML.
HyML MiNiMaL is meant to be used as a minimal codebase to generate XML (Extensible Markup Language) with the next features:
You can use HyML MiNiMaL for:
To compare with full version HyML XML / HTML macros, MiNiMaL means that there is no tag name validation and no tag and attribute minimize techniques utilized. If you need them, you should see full HyML documentation.
HyML thus refers to XML, althought markup language term itself is more generic term.
Project is hosted at: https://github.com/markomanninen/hyml
For easy install, use pip Python repository installer:
$ pip install hyml
This will install only the necessary source files for HyML, no example templates nor Jupyter Notebook files.
There are no other dependencies except Hy language upon Python of cource. If Hy does not exist on your computer, it will be installed (or updated to the version 0.12.1 or greater) at the same time.
Then import MiNiMaL macros:
(require (hyml.minimal (*)))
This will load the next macros for usage
ml
defvar
and deffun
macros for custom variable and function setterinclude
macro for using templateslist-comp*
list comprehension helper macroOptionally ml>
render macro will be loaded, if code is executed on Jupyter
Notebook / IPython environment with display.HTML
function available.
If you intend to use xml code indent
function, you should also import it:
(import (hyml.minimal (indent)))
And run the simple example:
(ml (tag :attr "value" (sub "Content")))
That should output:
<tag attr="value"><sub>Content</sub></tag>
To run basic tests, you can use Jupyter Notebook document for now.
If you want to play with HyML Notebook documents, you should download the whole HyML repository (or clone it with $ git clone https://github.com/markomanninen/hyml.git
) to your computer. It contains all necessary templates to get everything running as presented in the HyML MiNiMaL Notebook document.
Because codebase for HyML MiNiMaL implementation is roughly 60 lines only, it will be provided here with structural comments for introspection. More detailed comment are available in the minimal.hy source file.
In [1]:
; eval and compile variables, constants and functions for ml, defvar, deffun, and include macros
(eval-and-compile
; global registry for variables and functions
(setv variables-and-functions {})
; internal constants
(def **keyword** "keyword") (def **unquote** "unquote")
(def **splice** "unquote_splice") (def **unquote-splice** (, **unquote** **splice**))
(def **quote** "quote") (def **quasi** "quasiquote")
(def **quasi-quote** (, **quote** **quasi**))
; given two dictionaries, merge them into a new dictionary as a shallow copy
(defn merge-two-dicts [x y]
(if-not (or (empty? x) (empty? y))
; tiny optimization, if there are values in both x and y
(do (setv z (.copy x)) (.update z y) z)
; else one more check to return the one that has values
; or maybe all were empty? then either one should be fine
(if (empty? x) y x)))
; detach keywords and content from code expression
(defn get-content-attributes [code &optional [vars-and-funcs {}]]
(setv content [] attributes [] kwd None)
(for [item code]
(do (if (iterable? item)
(if (= (first item) **unquote**)
(setv item (eval (second item)
(merge-two-dicts variables-and-functions vars-and-funcs)))
(in (first item) **quasi-quote**) (setv item (name (eval item)))))
(if-not (keyword? item)
(if (none? kwd)
(.append content (parse-mnml item vars-and-funcs))
(.append attributes (, kwd (parse-mnml item vars-and-funcs)))))
(if (and (keyword? kwd) (keyword? item))
(.append attributes (, kwd (name kwd))))
(if (keyword? item) (setv kwd item) (setv kwd None))))
(if (keyword? kwd)
(.append attributes (, kwd (name kwd))))
(, content attributes))
; recursively parse expression
(defn parse-mnml [code &optional [vars-and-funcs {}]]
(if (coll? code)
(do (setv tag (catch-tag (first code)))
(if (in tag **unquote-splice**)
(if (= tag **unquote**)
(str (eval (second code) (merge-two-dicts variables-and-functions vars-and-funcs)))
(.join "" (map (if (empty? vars-and-funcs) parse-mnml (fn [item] (parse-mnml item vars-and-funcs)))
(eval (second code) (merge-two-dicts variables-and-functions vars-and-funcs)))))
(do (setv (, content attributes) (get-content-attributes (drop 1 code) vars-and-funcs))
(+ (tag-start tag attributes (empty? content))
(if (empty? content) ""
(+ (.join "" (map str content)) (+ "</" tag ">")))))))
(if (none? code) "" (str code))))
; detach tag from expression
(defn catch-tag [code]
(if (and (iterable? code) (= (first code) **unquote**))
(eval (second code))
(try (name (eval code))
(except (e Exception) (str code)))))
; concat attributes
(defn tag-attributes [attr]
(if (empty? attr) ""
(+ " " (.join " " (list-comp
(% "%s=\"%s\"" (, (name kwd) (name value))) [[kwd value] attr])))))
; create start tag
(defn tag-start [tag-name attr short]
(+ "<" tag-name (tag-attributes attr) (if short "/>" ">"))))
; global variable registry handler
(defmacro defvar [&rest args]
(setv l (len args) i 0)
(while (< i l) (do
(assoc variables-and-functions (get args i) (get args (inc i)))
(setv i (+ 2 i)))))
; global function registry handler
(defmacro deffun [name func]
(assoc variables-and-functions name (eval func)))
; include functionality for template engine
(defmacro include [template]
`(do (import [hy.importer [tokenize]])
(with [f (open ~template)]
(tokenize (+ "~@`(" (f.read) ")")))))
; main MiNiMaL macro to be used. passes code to parse-mnml
(defmacro ml [&rest code]
(.join "" (map parse-mnml code)))
;
(defmacro macro [name params &rest body]
`(do
(defmacro ~name ~params
`(quote ~~@body)) None))
Out[1]:
MiNiMaL macro syntax is simple and practically follows the rules of Hy syntax. MiNiMaL macro expression is made of the following four components:
Syntax of the expression consists of:
(
must have closing parentheses pair )
next items in the expression are either:
between keywords, keyword values, and content there must a whitespace separator OR expression components must be wrapped with double quotes when suitable
Only the tag name is mandatory on the expression. There is no limit on the depth of the nested levels. There is no limit on how many attribute-value pairs you want to use. Also it doesn't matter in what order you define tag content and keywords, althougt it might be easier to read for others, if the keywords are introduced first and then the content. However, all keywords are rendered in the same order they have been presented in the markup. Also content and sub nodes are rendered similarly in the given order.
Main differences to XML syntax are:
<
and >
delimiters, parentheses (
and )
are usedml
macro, given expressions does not need to have be inside a single root nodeIn addition to basic syntax, there are three other symbols for advanced code generation. They are:
~
~@
These all are symbols used in Hy macro notation, so they should be self explanatory. But to make everything clear, in the MiNiMaL macro they may look they work other way around.
Unquote (~
) and unquote-splice (~@
) gets you back to the Hy code evaluation mode. And quasiquote (`) sets you back to MiNiMaL macro mode. This is natural when you think that expression passed to MiNiMaL macro is a quoted code in the first place. So if you want to evaluate Hy code inside it, you need to do it inside unquote.
But let us start from the simple example first.
Observe the simple example utilizing above features and all four components:
(tag :attr "value" (sub "Content"))
tag
is the first element of the expression, so it regarded as a tag name. :attr "value"
is the keyword-value (attribute-value) -pair. (sub
starts a new expression. So there is no other content (or keywords) in the tag
element. Sub node instead has the titlecase content "Content"
given.
Running this on a console or on the Jupyter Notebook, output would be:
<tag attr="value"><sub>Content</sub></tag>
In [2]:
(ml (~(+ "t" "a" "g"))) ; output: tag
Out[2]:
This is useful if tag names collide with Hy internal symbols and datatypes. For example, the symbol J
is reserved for the complex number type. Instead of writing: (ml (J))
which produces <1j/>
, you should use: (ml ("J"))
or maybe (ml (~"J"))
.
In [3]:
(ml (tag ~(keyword (.join "" ['a 't 't 'r])) "value")) ; output: <tag attr="value"/>
Out[3]:
In [4]:
(ml (tag :attr ~(+ 'v 'a 'l 'u 'e))) ; output: <tag attr="value"/>
Out[4]:
In [5]:
(ml (tag ~(.upper "content"))) ; output: <tag>CONTENT</tag>
Out[5]:
In [6]:
; define variables with defvar macro
(defvar firstname "Dennis"
lastname "McDonald")
; define functions with deffun macro
(deffun wholename (fn [x y] (+ y ", " x)))
; use variables and functions with unquote / unquote splice
(ml (tag ~(wholename firstname lastname)))
Out[6]:
~@
)Unquote-splice is a special symbol to be used with the list and the template processing. It is perhaps the most powerful feature in the MiNiMaL macro.
You can use list comprehension function to generate a list of xml elements. Hy code, sub expressions, and variables / functions work inside unquote spliced expression. You need to quote a line, if it contains a sub MiNiMaL expression.
In [7]:
; generate 5 sub tags and use enumerated numeric value as a content
(ml (tag ~@(list-comp `(sub ~(str item)) [item (range 5)])))
Out[7]:
In [8]:
(with [f (open "templates/note.hy")] (print (f.read)))
Then we will define variables and a function to be used inside MiNiMaL macro:
In [9]:
(defvar to "Tove"
from "Jani"
heading "Reminder"
body "Don't forget me this weekend!")
And finally include and render the template. Indent function is imported and used to pretty print xml:
In [10]:
(import (hyml.minimal (indent)))
(print (indent (ml ~@(include "templates/note.hy"))))
On HyML package there is also the render-template
function and the extend-template
macro available via HyML.template
module.
HyML.template
is especially useful when embedding HyML MiNiMaL
to webserver, Flask for example. Here just the basic use case is shown, more examples you can find from sHyte 0.2 HyML Edition codebase.
In practice, render-template
function is a shortcut to call parse-mnml
with parameters and include
in sequence. The first argument in render-template
is the template name, and the rest of the arguments are dictionaries to be used on the template. So this is also an alternative way of using (bypassing the usage of) defvar
and deffun
macros.
In [11]:
; extend-template macro
(require [hyml.template [*]])
; render-template function
(import [hyml.template [*]])
; prepare template variables and functions
(setv template-variables-and-functions {"var" "Variable 1" "func" (fn[]"Function 1")})
; render template
(render-template "render.hyml" template-variables-and-functions)
Out[11]:
On template engines it is a common practice to extend sub template with main template. Say we have a layout template, that is used as a wapper for many other templates. We can refactor layout xml code to another file and keep the changing sub content on other files.
In HyML MiNiMaL
, extend-template
macro is used for that.
Lets show an example again. First we have the content of the layout.hyml
template file:
In [12]:
(with [f (open "templates/layout.hyml")] (print (f.read)))
And the content of the extend.hyml
sub template file:
In [13]:
(with [f (open "templates/extend.hyml")] (print (f.read)))
We have decided to set the title
as a "global" variable but define the body
on the sub template. The render process goes like this:
In [14]:
(setv locvar {"title" "Page title"})
(render-template "extend.hyml" locvar)
Out[14]:
Note that extension name .hyml
was used here even though it doesn't really matter what file extension is used.
At first it may look overly complicated and verbose to handle templates this way. Major advantage is found when processing multiple nested templates. Difference to simply including template files ~@(include "templates/template.hy")
is that on include
you pass variables (could be evaluated HyML
code) to template, but on extend-template
you pass unevaluated HyML
code to another template. This will add one more dynamic level on using HyML MiNiMaL
for XML content generation.
Template directory
Default template directory is set to "template/"
. You can change directory by changing template-dir
variable in the HyML.template
module:
(import hyml.template)
(def hyml.template.template-dir "templates-extra/")
Macro -macro
One more related feature to templates is a macro
-macro that can be used inside template files to factorize code for local purposes. If our template file would look like this:
~(macro custom [attr]
`(p :class ~attr))
~(custom ~class)
Then rendering it, would yield:
In [15]:
(render-template "macro.hyml" {"class" "main"}) ; output: <p class="main"/>
Out[15]:
In [16]:
(parse-mnml '(tag)) ; output: <tag/>
Out[16]:
Then let us make it a bit more complicated:
In [17]:
; define contacts dictionary
(defvar contacts [
{:firstname "Eric"
:lastname "Johnson"
:telephone "+1-202-555-0170"}
{:firstname "Mary"
:lastname "Johnson"
:telephone "+1-202-555-0185"}])
; pretty print
(print (indent
(ml
; root contacts node
(contacs
~@(do
; import parse-mnml function at the highest level of unquoted code
(import (hyml.minimal (parse-mnml)))
; contact node
(list-comp `(contact
; last contact detail node
~@(list-comp (parse-mnml `(~tag ~val))
[[tag val] (.items contact)]))
[contact contacts]))))))
With parse-mnml
it is also possible to pass an optional dictionary to be used for custom variables and functions on evaluation process. This is NOT possible with ml
-macro.
In [18]:
(parse-mnml '(tag :attr ~val) {"val" "val"}) ; output: <tag attr="val"/>
Out[18]:
With template
-macro you can actually see a very similar behaviour. In cases where variables can be hard coded, you might want to use this option:
In [19]:
(template {"val" "val"} `(tag :attr ~val)) ; output: <tag attr="val"/>
Out[19]:
It doesn't really matter in which order you pass expression and dictionary to the template
-macro. It is also ok to leave dictionary out if expression does not contain any variables. For template
-macro, expression needs to be quasiquoted, if it contains HyML
code.
In [20]:
; define variables
(defvar topic "How do you make XHTML 1.0 Transitional document with HyML?"
tags ['html 'xhtml 'hyml]
postedBy "Hege Refsnes"
contactEmail "hege.refsnes@example.com")
; define function. note that we want to return quasiquoted HyML code here
(deffun valid (fn []
`(p (a :href "http://validator.w3.org/check?uri=referer"
(img :src "http://www.w3.org/Icons/valid-xhtml10"
:alt "Valid XHTML 1.0 Transitional"
:height "31" :width "88")))))
; let just arficially create a body for the post
; and save it to the external template file
(with [f (open "templates/body.hy" "w")]
(f.write "(div :class \"body\"
\"I've been wondering if it is possible to create XHTML 1.0 Transitional
document by using a brand new HyML?\")"))
; print indented document as a pretty raw html
(print (indent
; start up the MiNiMaL macro
(ml
; xml document declaration
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"
\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
; create html tag with xml namespace and language attributes
(html :xmlns "http://www.w3.org/1999/xhtml" :lang "en"
(head
; title of the page
(title "Conforming XHTML 1.0 Transitional Template")
(meta :http-equiv "Content-Type" :content "text/html; charset=utf-8"))
(body
; wrap everything inside the post div
(div :class "post"
; first is the header of the post
(div :class "header" ~topic)
; then body of the post from external template file
~@(include "templates/body.hy")
; then the tags in spans
(div :class "tags"
~@(list-comp `(span ~tag) [tag tags]))
; finally the footer
(div :id "footer"
(p "Posted by: " ~postedBy)
(p "Email: "
(a :href ~(+ "mailto:" contactEmail) ~contactEmail) ".")))
; proceed valid stamp by a defined function
~(valid))))))
In [21]:
(ml (tag ~(+ "Generator inside: " (ml (sub "content")))))
Out[21]:
In [22]:
(ml (tag :alfred J. Kwak))
Out[22]:
In [23]:
[(ml ('tag)) (ml (`tag)) (ml (tag)) (ml ("tag"))]
Out[23]:
With keywords, however, single pre-queted strings will get parsed as a content.
In [24]:
[(ml (tag ':attr)) (ml (tag `:attr))]
Out[24]:
In [25]:
(ml (tag :"attr")) ; output: <tag ="attr"/>
Out[25]:
So only working version of keyword notation is :{symbol}
or unquoted ~(keyword {expression})
.
Note: keywords without value are interpreted as a keyword having the same value as the keyword name (called boolean attributes in HTML).
In [26]:
[(ml (tag :disabled)) (ml (tag ~(keyword "disabled"))) (ml (tag :disabled "disabled"))]
Out[26]:
If you wish to define multiple boolean attributes together with content, you can collect them at the end of the expression. Note that in XML boolean attributes cannot be minimized similar to HTML. Attributes always needs to have a value pair.
In [27]:
(ml (tag "Content" :disabled :enabled))
Out[27]:
One more thing with keywords is that if the same keyword value pair is given multiple times, it will show up in the mark up in the same order, as multiple. Depending on the markup parser, the last attribute might be valuated OR parser might give an error, because by XML Standard attibute names should be unique and not repeated under the same element.
In [28]:
(ml (tag :attr :attr "attr2"))
Out[28]:
In [29]:
;;;;;;;;;
; basic ;
;;;;;;;;;
; empty things
(assert (= (ml) ""))
(assert (= (ml"") ""))
(assert (= (ml "") ""))
(assert (= (ml ("")) "</>"))
; tag names
(assert (= (ml (tag)) "<tag/>"))
(assert (= (ml (TAG)) "<TAG/>"))
(assert (= (ml (~(.upper "tag"))) "<TAG/>"))
(assert (= (ml (tag "")) "<tag></tag>"))
; content cases
(assert (= (ml (tag "content")) "<tag>content</tag>"))
(assert (= (ml (tag "CONTENT")) "<tag>CONTENT</tag>"))
(assert (= (ml (tag ~(.upper "content"))) "<tag>CONTENT</tag>"))
; attribute names and values
(assert (= (ml (tag :attr "val")) "<tag attr=\"val\"/>"))
(assert (= (ml (tag ~(keyword "attr") "val")) "<tag attr=\"val\"/>"))
(assert (= (ml (tag :attr "val" "")) "<tag attr=\"val\"></tag>"))
(assert (= (ml (tag :attr "val" "content")) "<tag attr=\"val\">content</tag>"))
(assert (= (ml (tag :ATTR "val")) "<tag ATTR=\"val\"/>"))
(assert (= (ml (tag ~(keyword (.upper "attr")) "val")) "<tag ATTR=\"val\"/>"))
(assert (= (ml (tag :attr "VAL")) "<tag attr=\"VAL\"/>"))
(assert (= (ml (tag :attr ~(.upper "val"))) "<tag attr=\"VAL\"/>"))
; nested tags
(assert (= (ml (tag (sub))) "<tag><sub/></tag>"))
; unquote splice
(assert (= (ml (tag ~@(list-comp `(sub ~(str item)) [item [1 2 3]])))
"<tag><sub>1</sub><sub>2</sub><sub>3</sub></tag>"))
; variables
(defvar x "variable")
(assert (= (ml (tag ~x)) "<tag>variable</tag>"))
; functions
(deffun f (fn [x] x))
(assert (= (ml (tag ~(f "function"))) "<tag>function</tag>"))
; templates
(with [f (open "test/templates/test.hy" "w")] (f.write "(tag)"))
(assert (= (ml ~@(include "test/templates/test.hy")) "<tag/>"))
; set up custom template directory
(import hyml.template)
(def hyml.template.template-dir "test/templates/")
; render template function
(import [hyml.template [*]])
(with [f (open "test/templates/test2.hy" "w")] (f.write "(tag ~tag)"))
(assert (= (render-template "test2.hy" {"tag" "content"}) "<tag>content</tag>"))
; extend template
(require [hyml.template [*]])
(with [f (open "test/templates/test3.1.hy" "w")] (f.write "(tags :attr ~val ~tag)"))
(with [f (open "test/templates/test3.2.hy" "w")] (f.write "~(extend-template \"test3.1.hy\" {\"tag\" `(tag)})"))
(assert (= (render-template "test3.2.hy" {"val" "val"}) "<tags attr=\"val\"><tag/></tags>"))
; macro -macro
(with [f (open "test/templates/test4.hy" "w")] (f.write "~(macro custom [content] `(tag ~content))~(custom ~content)"))
(assert (= (render-template "test4.hy" {"content" "content"}) "<tag>content</tag>"))
; revert path
(def hyml.template.template-dir "templates/")
; template macro
(assert (= (template {"val" "val"} `(tag :attr ~val)) (template `(tag :attr ~val) {"val" "val"})))
;;;;;;;;;;;
; special ;
;;;;;;;;;;;
; tag names
(assert (= (ml (J)) "<1j/>"))
(assert (= (ml (~"J")) "<J/>"))
(assert (= [(ml ('tag)) (ml (`tag)) (ml (tag)) (ml ("tag"))] (* ["<tag/>"] 4)))
; attribute values
(assert (= [(ml (tag :attr 'val)) (ml (tag :attr `val)) (ml (tag :attr val)) (ml (tag :attr "val"))]
(* ["<tag attr=\"val\"/>"] 4)))
; content
(assert (= [(ml (tag 'val)) (ml (tag `val)) (ml (tag val)) (ml (tag "val"))]
(* ["<tag>val</tag>"] 4)))
; keyword processing
(assert (= [(ml (tag ':attr)) (ml (tag `:attr))] ["<tag>attr</tag>" "<tag>attr</tag>"]))
(assert (= (ml (tag :"attr")) "<tag =\"attr\"/>"))
; boolean attributes
(assert (= [(ml (tag :attr "attr")) (ml (tag :attr)) (ml (tag ~(keyword "attr")))]
["<tag attr=\"attr\"/>" "<tag attr=\"attr\"/>" "<tag attr=\"attr\"/>"]))
(assert (= (ml (tag :attr1 :attr2)) "<tag attr1=\"attr1\" attr2=\"attr2\"/>"))
(assert (= (ml (tag Content :attr1 :attr2)) "<tag attr1=\"attr1\" attr2=\"attr2\">Content</tag>"))
(assert (= (ml (tag :attr1 :attr2 Content)) "<tag attr1=\"attr1\" attr2=\"Content\"/>"))
; no space between attribute name and value as a string literal
(assert (= (ml (tag :attr"val")) "<tag attr=\"val\"/>"))
; no space between tag, keywords, keyword value, and content string literals
(assert (= (ml (tag"content":attr"val")) "<tag attr=\"val\">content</tag>"))
;;;;;;;;;
; weird ;
;;;;;;;;;
; quote should not be unquoted or surpressed
(assert (= (ml (quote :quote "quote" "quote")) "<quote quote=\"quote\">quote</quote>"))
; tag name, keyword name, value and content can be same
(assert (= (ml (tag :tag "tag" "tag")) "<tag tag=\"tag\">tag</tag>"))
; multiple same attribute names stays in the markup in the reserved order
(assert (= (ml (tag :attr "attr1" :attr "attr2")) "<tag attr=\"attr1\" attr=\"attr2\"/>"))
; parse-mnlm with variable and function dictionary
(assert (= (parse-mnml '(tag :attr ~var ~(func))
{"var" "val" "func" (fn[]"Content")}) "<tag attr=\"val\">Content</tag>"))
Copyright (c) 2017 Marko Manninen